package com.gitee.feizns.webtemplate.commons.data.order;

import com.baomidou.mybatisplus.MybatisDefaultParameterHandler;
import com.baomidou.mybatisplus.enums.DBType;
import com.baomidou.mybatisplus.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.plugins.pagination.DialectFactory;
import com.baomidou.mybatisplus.plugins.pagination.PageHelper;
import com.baomidou.mybatisplus.plugins.pagination.Pagination;
import com.baomidou.mybatisplus.plugins.parser.ISqlParser;
import com.baomidou.mybatisplus.plugins.parser.SqlInfo;
import com.baomidou.mybatisplus.toolkit.JdbcUtils;
import com.baomidou.mybatisplus.toolkit.PluginUtils;
import com.baomidou.mybatisplus.toolkit.SqlUtils;
import com.baomidou.mybatisplus.toolkit.StringUtils;
import com.gitee.feizns.webtemplate.commons.data.page.OrderPagination;
import com.gitee.feizns.webtemplate.commons.data.page.OrderRowBounds;
import com.gitee.feizns.webtemplate.commons.utils.reflect.FieldUtils;
import lombok.Getter;
import lombok.Setter;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.RowBounds;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 自定义排序规则
 * @see PaginationInterceptor
 * @author feizns
 * @since 2019/5/12 0012
 */
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class OrderByPaginationInterceptor extends PaginationInterceptor {

    /**
     * 日志
     */
    private static final Log logger = LogFactory.getLog(OrderByPaginationInterceptor.class);

    /**
     * 是否过滤查询中不存在的
     */
    @Getter
    @Setter
    private boolean filterNotExists = true;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        //==父类的变量
        ISqlParser sqlParser = FieldUtils.getFieldData(this, "sqlParser");

        boolean overflowCurrent = FieldUtils.getFieldData(this, "overflowCurrent");

        String dialectType = FieldUtils.getFieldData(this, "dialectType");;

        String dialectClazz = FieldUtils.getFieldData(this, "dialectClazz");

        boolean localPage = FieldUtils.getFieldData(this, "localPage");
        //==父类的变量

        StatementHandler statementHandler = (StatementHandler) PluginUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        this.sqlParser(metaObject);

        // 先判断是不是SELECT操作
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        if (!SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) {
            return invocation.proceed();
        }
        RowBounds rowBounds = (RowBounds) metaObject.getValue("delegate.rowBounds");
        /* 不需要分页的场合 */
        if (rowBounds == null || rowBounds == RowBounds.DEFAULT) {
            // 本地线程分页
            if ( localPage ) {
                // 采用ThreadLocal变量处理的分页
                rowBounds = PageHelper.getPagination();
                if (rowBounds == null) {
                    return invocation.proceed();
                }
            } else {
                // 无需分页
                return invocation.proceed();
            }
        }
        // 针对定义了rowBounds，做为mapper接口方法的参数
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
        String originalSql = boundSql.getSql();
        Connection connection = (Connection) invocation.getArgs()[0];
        DBType dbType = StringUtils.isNotEmpty(dialectType) ? DBType.getDBType(dialectType) : JdbcUtils.getDbType(connection.getMetaData().getURL());
        if ( rowBounds instanceof Pagination ) {
            Pagination page = (Pagination) rowBounds;
            boolean orderBy = true;
            if (page.isSearchCount()) {
                SqlInfo sqlInfo = SqlUtils.getOptimizeCountSql(page.isOptimizeCountSql(), sqlParser, originalSql);
                orderBy = sqlInfo.isOrderBy();
                this.queryTotal(overflowCurrent, sqlInfo.getSql(), mappedStatement, boundSql, page, connection);

                if ( rowBounds instanceof OrderPagination )
                    ((OrderPagination) rowBounds).setStart(page.getOffset());

                if (page.getTotal() <= 0) {
                    return invocation.proceed();
                }
            }

            String buildSql = concatOrderBy(originalSql, page, orderBy);

            originalSql = DialectFactory.buildPaginationSql(page, buildSql, dbType, dialectClazz);
        } else {
            // support physical Pagination for RowBounds

            if ( rowBounds instanceof OrderRowBounds && ((OrderRowBounds) rowBounds).isSearchCount() ) {
                SqlInfo sql = SqlUtils.getOptimizeCountSql(((OrderRowBounds) rowBounds).isOptimizeCountSql(), sqlParser, originalSql);
                long total = getTotal(sql.getSql(), mappedStatement, boundSql, connection);
                ((OrderRowBounds) rowBounds).setTotal(total);
                ((OrderRowBounds) rowBounds).setStart(rowBounds.getOffset());
                ((OrderRowBounds) rowBounds).setSize(rowBounds.getLimit());
                if ( total <= 0 )
                    return invocation.proceed();
            }

            String buildSql = originalSql;
            if ( rowBounds instanceof OrderBy ) {
                List<OrderColumn> orderBy = filterAndMarkUsed(originalSql, ((OrderBy) rowBounds).orderBy());
                buildSql = concatOrderBy(originalSql, orderBy);
            }

            originalSql = DialectFactory.buildPaginationSql(rowBounds, buildSql, dbType, dialectClazz);
        }

        /*
         * <p> 禁用内存分页 </p>
         * <p> 内存分页会查询所有结果出来处理（这个很吓人的），如果结果变化频繁这个数据还会不准。</p>
         */
        metaObject.setValue("delegate.boundSql.sql", originalSql);
        metaObject.setValue("delegate.rowBounds.offset", RowBounds.NO_ROW_OFFSET);
        metaObject.setValue("delegate.rowBounds.limit", RowBounds.NO_ROW_LIMIT);
        return invocation.proceed();
    }

    /**
     * 过滤并标记为使用
     * @param originalSql
     * @param orderColumns
     * @return
     */
    private List<OrderColumn> filterAndMarkUsed(String originalSql, List<OrderColumn> orderColumns) {
        if ( filterNotExists ) {
            orderColumns = filterNotExists(originalSql, orderColumns);
        }
        //标记为已使用
        orderColumns.stream().forEach(item -> item.setUsed(true));
        return orderColumns;
    }

    /**
     * 查询总数
     * @param sql
     * @param mappedStatement
     * @param boundSql
     * @param connection
     * @return
     */
    public long getTotal(String sql, MappedStatement mappedStatement, BoundSql boundSql, Connection connection) {
        try ( PreparedStatement statement = connection.prepareStatement(sql) ) {
            DefaultParameterHandler parameterHandler = new MybatisDefaultParameterHandler(mappedStatement, boundSql.getParameterObject(), boundSql);
            parameterHandler.setParameters(statement);
            try ( ResultSet resultSet = statement.executeQuery() ) {
                if ( resultSet.next() )
                    return resultSet.getLong(1);
            }
        } catch (Exception e) {
            logger.error("Error: Method queryTotal execution error !", e);
        }
        return 0;
    }

    /**
     * 拼接 ORDER BY
     * @param originalSql
     * @param orderColumns
     * @return
     */
    private String concatOrderBy(String originalSql, List<OrderColumn> orderColumns) {
        return originalSql + OrderUtils.getOrderByStrForSql(orderColumns);
    }

    /**
     *
     * @param originalSql
     * @param page
     * @param orderBy
     * @return
     */
    private String concatOrderBy(String originalSql, Pagination page, boolean orderBy) {
        if ( page instanceof OrderBy ) {
            if ( orderBy ) {
                List<OrderColumn> orderColumns = filterAndMarkUsed(originalSql, ((OrderBy) page).orderBy());
                return concatOrderBy(originalSql, orderColumns);
            }
        } else {
            return SqlUtils.concatOrderBy(originalSql, (Pagination) page, orderBy);
        }
        return originalSql;
    }

    /**
     * 过滤返回中没有的字段
     * @param originalSql
     * @param columns
     * @return
     */
    private List<OrderColumn> filterNotExists(String originalSql, List<OrderColumn> columns) {
        if ( originalSql.contains("*") == false ) {
            return columns.stream()
                    .filter(item -> originalSql.contains(item.getColumnName()))
                    .collect(Collectors.toList());
        }
        return columns;
    }

}
